package model;
import java.util.Iterator;


/**
 * The Maze class stores a grid of game tiles. It includes
 * functionality to add and remove tiles from the grid, and automatically
 * adds and removes the corresponding exits to these tiles. Walls
 * within the maze are simply modelled by the absence of exits. This
 * allows for one sided walls, portals, wrap around and all sorts
 * of other awesomeness. Maze class also supports iteration over the contained tiles.
 * 
 * @author Alexander Craig
 */
public class Maze implements Iterable<Tile> {
	
	/** stores the grid of maze tiles, indexed by their x,y coordinates */
	private Tile[][] mazeTiles;
	/** stores the maximum rows in the maze (from 0 to 1 less than this value) */
	private int maxX;
	/** stores the maximum columns in the maze (from 0 to 1 less than this value) */
	private int maxY;
	/** the required dots remaining to pass the maze */
	private int passDots;
	
	/**
	 * Generates a new maze with the specified maximum size
	 * @param maxX	The max x coordinate (number of columns)
	 * @param maxY The max y coordinate (number of rows)
	 * @param filled	determines whether the mazes should be filled
	 * 					with rooms, or if it should start as empty.
	 */
	public Maze(int maxX, int maxY, boolean filled) {
		mazeTiles = new Tile[maxX][maxY];
		passDots = 0;
		this.maxX = maxX;
		this.maxY = maxY;
		if (filled) {
			for (int x = 0; x < maxX; x++) {
				for (int y = 0; y < maxY; y++) {
					addTile(x,y);
				}
			}
		} else {
			clearMaze();
		}
	}
	
	/**
	 * Adds a new tile to the maze at the specified x, y position,
	 * given that the position is currently empty. Automatically adds
	 * exits to adjacent tiles.
	 * @param x	the x coordinate for the new tile
	 * @param y the y coordinate for the new tile
	 * @return	true if the add was successful, false if the position
	 * 			is already filled or invalid position passed
	 */
	public boolean addTile(int x, int y) {
		// Check for index out of bounds
		if (x < 0 || x >= maxX || y < 0 || y >= maxY) {
			return false;
		}
		// Check if position is already occupied
		if (mazeTiles[x][y] != null) {
			return false;
		} else {
			// Generate the new tile and add it to the maze
			Tile newTile = new Tile(x, y);
			mazeTiles[x][y] = newTile;
			// Automatically connect this tile to all adjacent tiles
			for (Direction d : Direction.values()) {
				addExit(x, y, d, true);
			}
			return true;
		}
	}
	
	/**
	 * Adds a new tile to the maze at the specified x, y position,
	 * given that the position is currently empty. Does not add
	 * exits in each direction
	 * @param x	the x coordinate for the new tile
	 * @param y the y coordinate for the new tile
	 * @return	true if the add was successful, false if the position
	 * 			is already filled or invalid position passed
	 */
	public boolean addExitlessTile(int x, int y) {
		// Check for index out of bounds
		if (x < 0 || x >= maxX || y < 0 || y >= maxY) {
			return false;
		}
		// Check if position is already occupied
		if (mazeTiles[x][y] != null) {
			return false;
		} else {
			// Generate the new tile and add it to the maze
			Tile newTile = new Tile(x, y);
			mazeTiles[x][y] = newTile;
			return true;
		}
	}
	
	/**
	 * Removes the tile at the specified x, y coordinates
	 * @param x	the x coordinate of the tile to be removed
	 * @param y	the y coordinate of the tile to be removed
	 * @return	true if the tile was successfully removed, false if
	 * 			no tile existed at the given position
	 */
	public boolean removeTile(int x, int y) {
		// Check for index out of bounds
		if (x < 0 || x >= maxX || y < 0 || y >= maxY) {
			return false;
		}
		// Check to make sure the tile actually exists
		if (mazeTiles[x][y] == null) {
			return false;
		} else {
			Tile delTile = mazeTiles[x][y];
			// Automatically remove all exits pointing at this tile
			for (Tile t : this) {
				if(t != null) {
					for (Direction d : Direction.values()) {
						if (t.getExit(d) == delTile) {
							t.removeExit(d);
						}
					}
				}
			}
			mazeTiles[x][y] = null;
			return true;
		}
	}
	
	/**
	 * Returns the tile at the specified x, y coordinates
	 * @param x	the x coordinate of the desired tile
	 * @param y	the y coordinate of the desired tile
	 * @return	null if no tile was found at the specified location or if
	 * 			the index was invalid
	 */
	public Tile getTile(int x, int y) {
		// Check for index out of bounds
		if (x < 0 || x >= maxX || y < 0 || y >= maxY) {
			return null;
		}
		return mazeTiles[x][y];
	}
	
	
	/**
	 * Gets the tile neighbouring a given tile in the maze.
	 * @param curTile	the tile to find a neighbour for
	 * @param dir	the direction to look for a neighbour in
	 * @return	null if no neighbour exists, or the neighbour tile
	 */
	public Tile getNeighbour(Tile curTile, Direction dir) {
		int x, y;
		switch (dir) {
		case NORTH:
			x = curTile.getXCoord();
			y = curTile.getYCoord() - 1; 
			break;
		case EAST:
			x = curTile.getXCoord() + 1; 
			y = curTile.getYCoord(); 
			break;
		case SOUTH:
			x = curTile.getXCoord();
			y = curTile.getYCoord() + 1;
			break;
		case WEST:
			x = curTile.getXCoord() - 1;
			y = curTile.getYCoord(); 
			break;
		default:
			return null;
		}
		if (x < 0 || x >= maxX || y < 0 || y >= maxY) {
			return null;
		}
		return mazeTiles[x][y];
	}
	
	/**
	 * Clears the maze of all tiles.
	 */
	public void clearMaze() {
		for (int x = 0; x < maxX; x++) {
			for (int y = 0; y < maxY; y++) {
				mazeTiles[x][y] = null;
			}
		}
	}
	
	/**
	 * Resets all the dots in the maze.
	 */
	public void resetMaze() {
		for(Tile t : this) {
			if( t != null )
				t.resetDot();
		}
	}
	
	/**
	 * @return the number of unvisited tiles (dots) remaining in the maze
	 */
	public int dotsRemaining() {
		int total = 0;
		for (Tile t : this) {
			if (t != null && t.hasDot()) {
				total++;
			}
		}
		return total;
	}
	
	/**
	 * @return	true if the maze has the less than or equal to the remaining 
	 * 			number of dots required to clear the maze, false if the maze
	 * 			cleared
	 */
	public boolean isComplete() {
		return dotsRemaining() <= passDots;
	}
	
	/**
	 * Adds an exit to the specified tile in the specified direction. Also
	 * can add a return exit from the neighbouring tile to the current tile.
	 * Requires that another tile already exists in the specified direction.
	 * @param x	the x coordinate of the tile
	 * @param y	the y coordinate of the tile
	 * @param dir	the direction to add an exit in
	 * @param linkBack	determines whether a matching exit should be added to the
	 * 					neighbouring tile
	 * @return	false if the add failed for any reason (ie. tile does not exist,
	 * 			neighbour does not exist, exits already exist)
	 */
	public boolean addExit(int x, int y, Direction dir, boolean linkBack) {
		// Get the first point and ensure a tile exists
		Tile curTile = getTile(x, y);
		if (curTile == null) return false;
		
		// Get the neighbour coordinates and ensure a tile exists
		Tile newTile = getNeighbour(curTile, dir);
		if (newTile == null) return false;
		
		// Ensure no exits already exist
		// if(curTile.getExit(dir) != null) return false;
		// if (newTile.getExit(dir.getOpposite()) != null && linkBack) return false;

		// Add the exits
		curTile.setExit(dir, newTile);
		if (linkBack) {
			newTile.setExit(dir.getOpposite(), curTile);
		}
		return true;
	}
	
	/**
	 * Adds a portal from the first tile to the second, in the specified direction.
	 * @param x1	the x coordinate of the source tile
	 * @param y1	the y coordinate of the source tile
	 * @param x2	the x coordinate of the destination tile
	 * @param y2	the y coordinate of the destination tile
	 * @param dir	the direction to add the exit in (in the source tile)
	 * @return	true if the portal is added, false if it fails for any reason
	 */
	public boolean addPortal(int x1, int y1, int x2, int y2, Direction dir) {
		Tile source = getTile(x1, y1);
		Tile destination = getTile(x2, y2);
		if (source == null || destination == null) {
			return false;
		} else {
			return source.setExit(dir, destination);
		}
	}
	
	/**
	 * Removes an exit from the specified tile. Also can remove the
	 * return exit from the neighbouring tile to the current tile.
	 * Requires that the tile exist, and that it has an exit in the given direction.
	 * @param x	the x coordinate of the tile
	 * @param y	the y coordinate of the tile
	 * @param dir	the direction to remove an exit from
	 * @param linkBack	determines whether the matching exit on the neighbouring 
	 * 					tile should be removed
	 * @return	false if the remove failed for any reason (ie. tile does not exist,
	 * 			exits do not exist)
	 */
	public boolean removeExit(int x, int y, Direction dir, boolean linkBack) {
		// Get the first point and ensure a tile exists
		Tile curTile = getTile(x, y);
		if (curTile == null) return false;
		
		// Get the neighbour coordinates and ensure a tile exists
		Tile newTile = null;
		if (linkBack) {
			newTile = getNeighbour(curTile, dir);
		}
		
		// Ensure exits actually exist to delete
		if(curTile.getExit(dir) == null) return false;

		// Remove the exits
		curTile.removeExit(dir);
		if (linkBack) {
			if (newTile == null) {
				return false;
			}
			newTile.removeExit(dir.getOpposite());
		}
		return true;
	}
	
	/**
	 * @return the maximum x coordinate in this maze (from 0 to maxX - 1)
	 */
	public int getMaxX()
	{
		return maxX;
	}
	
	/**
	 * @return the maximum y coordinate in this maze (from 0 to maxY - 1)
	 */
	public int getMaxY()
	{
		return maxY;
	}
	
	@Override
	/**
	 * Allows client classes to iterate over the available point coordinates for tiles.
	 */
	public Iterator<Tile> iterator() {
		return new MazeIterator(maxX, maxY, mazeTiles);
	}
	
	/**
	 * Sets the number of dots remaining needed to pass the maze
	 * @param dots	the number of dots remaining
	 */
	public void setPassDots(int dots) {
		if (dots >= 0) {
			passDots = dots;
		}
	}
	
	/**
	 * @return the number of dots remaining needed to pass the maze
	 */
	public int getPassDots() {
		return passDots;
	}

}
